# Copyright 2020 by Kurt Rathjen. All Rights Reserved.
#
# This library is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. This library is distributed in the
# hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
"""
Base object for saving poses, animation, selection sets and mirror tables.

Example:
	import FXmutils

	t = FXmutils.TransferObject.fromPath("/tmp/pose.json")
	t = FXmutils.TransferObject.fromObjects(["object1", "object2"])

	t.load(selection=True)
	t.load(objects=["obj1", "obj2"])
	t.load(namespaces=["namespace1", "namespace2"])

	t.save("/tmp/pose.json")
	t.read("/tmp/pose.json")
"""
import os
import abc
import json
import time
import locale
import getpass
import logging

from FXstudiovendor import six

import FXmutils

try:
	import maya.cmds
except Exception:
	import traceback
	traceback.print_exc()


logger = logging.getLogger(__name__)


class TransferObject(object):

	@classmethod
	def fromPath(cls, path):
		"""
		Return a new transfer instance for the given path.

		:type path: str
		:rtype: TransferObject
		"""
		t = cls()
		t.setPath(path)
		t.read()
		return t

	@classmethod
	def fromObjects(cls, objects, **kwargs):
		"""
		Return a new transfer instance for the given objects.

		:type objects: list[str]
		:rtype: TransferObject
		"""
		t = cls(**kwargs)
		for obj in objects:
			t.add(obj)
		return t

	@staticmethod
	def readJson(path):
		"""
		Read the given json path

		:type path: str
		:rtype: dict
		"""
		with open(path, "r") as f:
			data = f.read() or "{}"

		data = json.loads(data)

		return data

	@staticmethod
	def readList(path):
		"""
		Legacy method for reading older .list file type.

		:rtype: dict
		"""
		with open(path, "r") as f:
			data = f.read()

		data = eval(data, {})
		result = {}
		for obj in data:
			result.setdefault(obj, {})

		return {"objects": result}

	@staticmethod
	def readDict(path):
		"""
		Legacy method for reading older .dict file type.

		:rtype: dict
		"""
		with open(path, "r") as f:
			data = f.read()

		data = eval(data, {})
		result = {}
		for obj in data:
			result.setdefault(obj, {"attrs": {}})
			for attr in data[obj]:
				typ, val = data[obj][attr]
				result[obj]["attrs"][attr] = {"type": typ, "value": val}

		return {"objects": result}

	def __init__(self):
		self._path = None
		self._namespaces = None
		self._data = {"metadata": {}, "objects": {}}

	def path(self):
		"""
		Return the disc location for the transfer object.

		:rtype: str
		"""
		return self._path

	def setPath(self, path):
		"""
		Set the disc location for loading and saving the transfer object.

		:type path: str
		"""
		dictPath = path.replace(".json", ".dict")
		listPath = path.replace(".json", ".list")

		if not os.path.exists(path):

			if os.path.exists(dictPath):
				path = dictPath

			elif os.path.exists(listPath):
				path = listPath

		self._path = path

	def validate(self, **kwargs):
		"""
		Validate the given kwargs for the current IO object.

		:type kwargs: dict
		"""
		namespaces = kwargs.get("namespaces")
		if namespaces is not None:
			sceneNamespaces = FXmutils.namespace.getAll() + [":"]
			for namespace in namespaces:
				if namespace and namespace not in sceneNamespaces:
					msg = 'The namespace "{0}" does not exist in the scene! ' \
						  "Please choose a namespace which exists."
					msg = msg.format(namespace)
					raise ValueError(msg)

	def mtime(self):
		"""
		Return the modification datetime of self.path().

		:rtype: float
		"""
		return os.path.getmtime(self.path())

	def ctime(self):
		"""
		Return the creation datetime of self.path().

		:rtype: float
		"""
		return os.path.getctime(self.path())

	def data(self):
		"""
		Return all the data for the transfer object.

		:rtype: dict
		"""
		return self._data

	def setData(self, data):
		"""
		Set the data for the transfer object.

		:type data:
		"""
		self._data = data

	def owner(self):
		"""
		Return the user who created this item.

		:rtype: str
		"""
		return self.metadata().get("user", "")

	def description(self):
		"""
		Return the user description for this item.

		:rtype: str
		"""
		return self.metadata().get("description", "")

	def objects(self):
		"""
		Return all the object data.

		:rtype: dict
		"""
		return self.data().get("objects", {})

	def object(self, name):
		"""
		Return the data for the given object name.

		:type name: str
		:rtype: dict
		"""
		return self.objects().get(name, {})

	def createObjectData(self, name):
		"""
		Create the object data for the given object name.

		:type name: str
		:rtype: dict
		"""
		return {}

	def namespaces(self):
		"""
		Return the namespaces contained in the transfer object

		:rtype: list[str]
		"""
		if self._namespaces is None:
			group = FXmutils.groupObjects(self.objects())
			self._namespaces = group.keys()

		return self._namespaces

	def objectCount(self):
		"""
		Return the number of objects in the transfer object.

		:rtype: int
		"""
		return len(self.objects() or [])

	def add(self, objects):
		"""
		Add the given objects to the transfer object.

		:type objects: str | list[str]
		"""
		if isinstance(objects, six.string_types):
			objects = [objects]

		for name in objects:
			self.objects()[name] = self.createObjectData(name)

	def remove(self, objects):
		"""
		Remove the given objects to the transfer object.

		:type objects: str | list[str]
		"""
		if isinstance(objects, six.string_types):
			objects = [objects]

		for obj in objects:
			del self.objects()[obj]

	def setMetadata(self, key, value):
		"""
		Set the given key and value in the metadata.

		:type key: str
		:type value: int | str | float | dict
		"""
		self.data()["metadata"][key] = value

	def updateMetadata(self, metadata):
		"""
		Update the given key and value in the metadata.

		:type metadata: dict
		"""
		self.data()["metadata"].update(metadata)

	def metadata(self):
		"""
		Return the current metadata for the transfer object.

		Example: print(self.metadata())
			Result # {
				"User": "",
				"Scene": "",
				"Reference": {"filename": "", "namespace": ""},
				"Description": "",
			}

		:rtype: dict
		"""
		return self.data().get("metadata", {})

	def read(self, path=""):
		"""
		Return the data from the path set on the Transfer object.

		:type path: str
		:rtype: dict
		"""
		path = path or self.path()

		if path.endswith(".dict"):
			data = self.readDict(path)

		elif path.endswith(".list"):
			data = self.readList(path)

		else:
			data = self.readJson(path)

		self.setData(data)

	@abc.abstractmethod
	def load(self, *args, **kwargs):
		pass

	@FXmutils.showWaitCursor
	def save(self, path):
		"""
		Save the current metadata and object data to the given path.

		:type path: str
		:rtype: None
		"""
		logger.info("Saving pose: %s" % path)

		user = getpass.getuser()
		if user:
			user = six.text_type(user)

		ctime = str(time.time()).split(".")[0]
		references = FXmutils.getReferenceData(self.objects())

		self.setMetadata("user", user)
		self.setMetadata("ctime", ctime)
		self.setMetadata("version", "1.0.0")
		self.setMetadata("references", references)
		self.setMetadata("mayaVersion", maya.cmds.about(v=True))
		self.setMetadata("mayaSceneFile", maya.cmds.file(q=True, sn=True))

		# Move the metadata information to the top of the file
		metadata = {"metadata": self.metadata()}
		data = self.dump(metadata)[:-1] + ","

		# Move the objects information to after the metadata
		objects = {"objects": self.objects()}
		data += self.dump(objects)[1:]

		# Create the given directory if it doesn't exist
		dirname = os.path.dirname(path)
		if not os.path.exists(dirname):
			os.makedirs(dirname)

		with open(path, "w") as f:
			f.write(str(data))

		logger.info("Saved pose: %s" % path)

	def dump(self, data=None):
		"""
		:type data: str | dict
		:rtype: str
		"""
		if data is None:
			data = self.data()

		return json.dumps(data, indent=2)
